Skip to content

Next release: feat: Implement DevicesHistory feature with triggers and history tracking#1701

Merged
jokob-sk merged 8 commits into
mainfrom
next_release
Jul 6, 2026
Merged

Next release: feat: Implement DevicesHistory feature with triggers and history tracking#1701
jokob-sk merged 8 commits into
mainfrom
next_release

Conversation

@jokob-sk

@jokob-sk jokob-sk commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added a device Change Log view with filter dropdowns (changed-by/changed-column), searchable grouped events, and device links.
    • Added a History tab to device details and navigation to the Change Log page.
    • Introduced history retention controls (enable/disable, tracked fields) to govern what gets recorded and for how long.
  • Documentation

    • Expanded documentation for device auditing/change history, presence view, and settings development.
    • Updated API docs with device history queries and filter value endpoints.
  • Bug Fixes

    • Improved device identifier consistency and ensured history data can be pruned reliably.

jokob-sk added 2 commits July 4, 2026 23:34
…king

- Added db_history.py to manage DevicesHistory table and triggers for INSERT and UPDATE operations.
- Created device_history_instance.py for querying and grouping DevicesHistory records.
- Developed change_history.php for displaying device change history with filtering and pagination.
- Introduced skel_device_details_tab_history.php for skeleton loading state in device details tab.
- Added unit tests in test_device_history.py to validate trigger functionality and history management.
- Implemented filter population and pagination in the change history UI.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03bcce9b-7409-4d91-9594-606ec5c96a9e

📥 Commits

Reviewing files that changed from the base of the PR and between 2dd7904 and cf6a29a.

📒 Files selected for processing (2)
  • front/css/app.css
  • front/plugins/db_cleanup/script.py

📝 Walkthrough

Walkthrough

Adds device-change auditing, query/filter endpoints, a new Change Log UI, retention settings, localized strings, and related documentation. Also updates the database cleanup flow and backend tests to cover history tracking and pruning.

Changes

Device Change History Feature

Layer / File(s) Summary
Retention and device identity settings
back/app.conf, server/initialise.py, server/models/device_instance.py
Adds DEV_HIST_DAYS and DEV_HIST_TRACKED settings and generates a UUID devGUID for new devices when missing.
History schema, triggers, and DB bootstrap
server/db/db_history.py, server/database.py
Creates DevicesHistory, defines SQLite audit triggers, and wires table/trigger setup into database initialization.
History read/prune model
server/models/device_history_instance.py
Adds grouped-history queries, filter discovery, total counts, and retention pruning for DevicesHistory.
API surface for history data
server/api_server/graphql_types.py, server/api_server/graphql_helpers.py, server/api_server/graphql_endpoint.py, server/api_server/api_server_start.py
Adds GraphQL history types/query support, paging normalization, and the REST filter-values endpoint.
Change Log UI and device tab wiring
front/changeLog.php, front/changeLogCore.php, front/deviceDetails.php, front/php/templates/skel_change_log.php, front/js/common.js, front/js/ui_components.js, front/js/cache.js, front/css/app.css, front/php/templates/header.php, front/pluginsCore.php, front/workflows.php
Adds the Change Log page, history tab, lazy loading, table/filter behavior, skeleton UI, device lookup helper, navigation, and supporting styling.
Cleanup, tests, and docs
front/plugins/db_cleanup/script.py, test/backend/test_device_history.py, test/db_test_helpers.py, docs/API_GRAPHQL.md, docs/PERFORMANCE.md, docs/DEVICE_CHANGE_LOG.md, docs/WHERE_NETALERTX_FITS.md, docs/index.md, docs/SETTINGS_SYSTEM.md, mkdocs.yml
Prunes history during cleanup, adds backend coverage, and documents the new API, retention behavior, and product/navigation pages.
Localization
front/php/templates/language/*.json
Adds device-history, navigation, workflow, and settings translation keys across supported locales.
Presence view updates
docs/DEVICE_PRESENCE.md, front/deviceDetailsPresence.php, front/presence.php, docs/stylesheets/custom.css
Adds presence-view documentation, help links, table text, and presence-state styling.

AI Skill Guide Documentation Updates

Layer / File(s) Summary
Database patterns skill and references
.github/skills/database-patterns/SKILL.md, .gemini/skills/initiative-start/reasearch-skill.md, .github/copilot-instructions.md
Adds database-patterns guidance and references it from the skill guidance index and Copilot instructions.
Settings management skill guides
.gemini/skills/settings/SKILL.md, .github/skills/settings-management/SKILL.md
Adds and expands settings-development guidance for Gemini and Copilot.
Skills cross-reference pages
.gemini/skills/skills-index/SKILL.md, .github/skills/skills-overview/SKILL.md, .github/copilot-instructions.md
Adds cross-reference pages mapping Gemini and Copilot skills.
Plugin, navigation, testing, and MCP skills
.gemini/skills/plugin-development/plugin-skill.md, .gemini/skills/project-navigation/SKILL.md, .gemini/skills/testing-workflow/SKILL.md, .github/skills/mcp-activation/SKILL.md
Expands plugin, navigation, testing, and activation guidance with structured usage and environment content.

Possibly related PRs

  • netalertx/NetAlertX#1435: Adds and governs the Devices.*Source provenance fields that this PR uses for changedBy attribution in history triggers.
  • netalertx/NetAlertX#1508: Touches server/database.py initialization flow, which this PR also extends with schema/trigger setup.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: adding DevicesHistory with triggers and history tracking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch next_release

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jokob-sk jokob-sk changed the title Next release Next release: feat: Implement DevicesHistory feature with triggers and history tracking Jul 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (8)
docs/SETTINGS_SYSTEM.md (1)

58-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Show both localization hooks in the example.

The example only demonstrates the name lookup, but the new settings strings already exist as both <KEY>_name and <KEY>_description. Including both references here will keep new plugin settings from being half-localized.

♻️ Proposed doc fix
 "name": [
   {
     "string": "_GLOBAL_LANG_FILES_"
   }
-]
+],
+"description": [
+  {
+    "string": "_GLOBAL_LANG_FILES_"
+  }
+]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/SETTINGS_SYSTEM.md` around lines 58 - 66, The SETTINGS_SYSTEM example
only shows the `name` localization hook and omits the matching `description`
hook, so update the example to demonstrate both references together. Use the
existing localization pattern in the doc around the `config.json` example to
show how both `<KEY>_name` and `<KEY>_description` should be declared, so
readers can localize plugin settings fully.
.gemini/skills/testing-workflow/SKILL.md (2)

29-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use repo-relative commands in the examples.

Every command example here hard-codes /workspaces/NetAlertX (and /workspaces/NetAlertX/.devcontainer/...). That contradicts the repo-root convention for Gemini skills and makes the examples brittle. Please switch these to relative paths or explicitly scope them to the devcontainer. Based on learnings: command examples in .gemini/skills should omit the absolute /workspaces/NetAlertX prefix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gemini/skills/testing-workflow/SKILL.md around lines 29 - 85, The command
examples in the testing workflow skill hard-code the repository root, which
conflicts with the repo-relative convention. Update the examples in SKILL.md to
use relative paths for pytest and setup commands, and ensure any references tied
to the testing flow use the same repo-root style consistently. Locate the
affected examples around the testing sections and the Authentication &
Environment Reset steps.

Source: Learnings


19-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the Copilot-only skill handoff.

.gemini/skills docs should not instruct readers to call activate_skill("devcontainer-management"); that's a Copilot/devcontainer workflow, not a Gemini CLI step. Please restate this as plain devcontainer guidance or move it to the Copilot skill. Based on learnings: Gemini-side skill docs should only reference Gemini-available tools.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gemini/skills/testing-workflow/SKILL.md around lines 19 - 23, The
testing-workflow skill currently contains a Copilot-specific handoff that tells
readers to call activate_skill("devcontainer-management"), which is not valid
Gemini CLI guidance. Update the relevant guidance in SKILL.md to describe the
devcontainer step in plain language using Gemini-available tooling, or remove it
from this skill and relocate it to the Copilot-side skill; keep the surrounding
host-machine/container detection logic consistent with the existing
testing-workflow instructions.

Source: Learnings

.gemini/skills/skills-index/SKILL.md (1)

21-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid naming testFailure in the Gemini-side index.

This row pulls a devcontainer-only tool into a .gemini/skills document. If the goal is just to point to the richer Copilot counterpart, keep the description environment-neutral so the Gemini index doesn't advertise tools it can't use. Based on learnings: Gemini-side skill docs should avoid devcontainer-only tool references.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gemini/skills/skills-index/SKILL.md at line 21, The `testing-workflow` row
in `SKILL.md` should not mention the devcontainer-only `testFailure` tool or
other Copilot-specific tooling. Update the `Testing` entry to keep the
Gemini-side index environment-neutral while still pointing to the richer
counterpart, so the `testing-workflow` description only references capabilities
available in Gemini.

Source: Learnings

server/db/db_history.py (2)

28-74: 📐 Maintainability & Code Quality | 🔵 Trivial

Single source of truth for tracked-field list would prevent drift.

_HIST_FIELDS here, _DEV_HIST_TRACKED_DEFAULT in server/initialise.py, and the DEV_HIST_TRACKED default in back/app.conf must all be kept in sync by hand. They currently match (41 fields each), but any future addition/removal in one place without the others will silently desync "trackable" fields from "actually audited" fields.

Consider deriving _HIST_FIELDS's field-name list (or the settings default) from a single shared constant module both files import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/db/db_history.py` around lines 28 - 74, The tracked-field definitions
are duplicated across _HIST_FIELDS in db_history, _DEV_HIST_TRACKED in
initialise, and the DEV_HIST_TRACKED default, so they can drift apart. Introduce
a single shared source for the field-name list (for example, a constant module
imported by db_history and initialise) and have the settings default derive from
that shared list instead of hardcoding it in multiple places. Keep the existing
symbols _HIST_FIELDS and _DEV_HIST_TRACKED aligned through that shared constant.

85-88: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Per-field Settings subquery repeated up to 41× per Devices write.

Each of the 41 per-field INSERT..SELECT blocks re-runs the _TRACKED_CHECK subquery against Settings independently, in addition to the single _HIST_DAYS_GUARD subquery in the trigger's WHEN clause. On every Devices row UPDATE/INSERT (which happens on every scan cycle for active devices) this issues up to 41 near-duplicate lookups against the Settings table.

💡 Possible direction

Consider normalizing DEV_HIST_TRACKED into a dedicated lookup structure (e.g., a small DevHistTrackedFields table populated on settings import) and joining/EXISTS against it once per trigger firing, rather than doing 41 independent instr() string searches against a serialized settings value on every row write.

Also applies to: 91-143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/db/db_history.py` around lines 85 - 88, The Devices history trigger is
repeatedly re-running the same DEV_HIST_TRACKED lookup for each per-field block,
causing many duplicate Settings subqueries on every write. Update the db_history
trigger generation around _TRACKED_CHECK and the per-field INSERT..SELECT blocks
to resolve tracked fields once per trigger firing, ideally by using a dedicated
lookup structure such as DevHistTrackedFields or an equivalent precomputed
EXISTS/join check, and then reuse that result across all field writes.
server/database.py (1)

242-247: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoff

Fragile trigger-recreation coupling with AppEvent_obj.drop_all_triggers().

The re-creation after AppEvent_obj(self) correctly compensates for the trigger wipe, but it relies on every future trigger-creating module remembering to hook in similarly after AppEvent_obj. Worth considering, longer-term, having drop_all_triggers() scoped to only the triggers it actually owns (or an explicit trigger registry) instead of an indiscriminate drop-all.

Not blocking — the current fix is correct and well-commented.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/database.py` around lines 242 - 247, The trigger recreation in
database initialization is currently compensating for
AppEvent_obj.drop_all_triggers() wiping unrelated triggers, which creates
fragile coupling. Update the trigger cleanup flow so
AppEvent_obj.drop_all_triggers() only removes triggers it owns, or introduce an
explicit trigger registry/ownership check, and keep
ensure_deviceshistory_triggers(self.sql) as the device-history re-create path
tied to the database bootstrap sequence.
server/api_server/graphql_endpoint.py (1)

109-109: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Blind except Exception swallows real errors with minimal diagnostics.

Ruff flags both catches (BLE001). Returning an empty DeviceHistoryResult on any failure (including bugs/DB errors) makes the UI look like "no history" instead of surfacing a real failure, and only {e} is logged (no traceback), making root-causing production issues harder.

Also applies to: 131-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api_server/graphql_endpoint.py` at line 109, The broad exception
handlers in the GraphQL endpoint are hiding real failures and only logging a
minimal message. Replace the two `except Exception` blocks in the device history
resolver path with narrower error handling or re-raise unexpected errors, and
keep returning `DeviceHistoryResult` only for the intended “no data” cases. In
the logging path, include full exception context/traceback instead of just `{e}`
so failures in the `graphql_endpoint` flow are diagnosable.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gemini/skills/initiative-start/reasearch-skill.md:
- Around line 24-26: The skill reference examples in this document use an
incorrect repo-root path prefix, so update the `.gemini/skills` guidance to
reference `.github/...` without the leading slash. Fix the path strings in the
relevant list items in `reasearch-skill.md`, keeping the examples consistent
with the rest of the skill docs and the repository-root-relative convention.

In `@front/change_history.php`:
- Line 42: The change history table header for Device is hardcoded instead of
using localization like the other headers. Update the table header in the change
history view to use the existing lang(...) translation pattern, matching the
surrounding header labels in the same table.
- Around line 109-123: Both AJAX requests in the change history flow are missing
failure handling, so the loading state can persist indefinitely; update the
request logic in the change history code path to add an error callback for each
$.ajax call, and make sure it clears the spinner/loading row in
`#changeHistoryBody` and shows a user-facing failure state when the request in
change_history.php fails. Keep the fix in the same AJAX handlers that currently
call renderTable and updatePagination so both the initial load and the later
request paths handle auth, network, and server errors consistently.
- Around line 25-32: The change history filters in the two select elements
currently call loadChangeHistory() without resetting pagination, which can leave
the table empty when a filter shrinks the result set. Update the filter change
handling in change_history.php so that the pagination state variable _chPage is
reset to the first page before loadChangeHistory() runs, and make sure both
filter controls use the same reset behavior.
- Line 110: The change history page is hardcoding backend URLs instead of
honoring the configured API base. Update the URL construction in
change_history.php so both the GraphQL endpoint and the history filters endpoint
are built from getApiBase(), matching the other history pages and avoiding
direct use of /server/graphql and /server/devices/history/filters.
- Around line 133-148: The device link in the change history table is still
pointing to an empty mac query, so it cannot open the correct device details.
Update the anchor generation in change_history.php where the row HTML is built
to pass the actual device identifier expected by deviceDetails.php, using the
same value already available as g.devGUID (or the proper MAC field if present),
and remove the unused data-guid attribute unless it is needed elsewhere.

In `@front/deviceDetails.php`:
- Around line 687-707: The AJAX request in the history loading flow is missing
an error handler, so failures leave the inline spinner and history skeleton
stuck. Update the $.ajax call in the device history logic to include an error
callback alongside success, and use it to clear the loading state for
historyTableBody and hide `#skel-tab-history` on auth/network/5xx failures. Keep
the fix localized to the history fetch routine that calls _renderHistoryTable,
_updateHistoryPagination, and _populateHistoryFilters so all failure paths clean
up the UI.
- Around line 174-181: The history filter dropdowns in the device details view
trigger loadHistoryData() without resetting _histPage, so changing a filter can
keep the user on an out-of-range page. Update the onchange behavior for
histFilterBy and histFilterCol so the pagination state is reset before reloading
history, matching the fix used in change_history.php and ensuring
loadHistoryData() starts from the first page after any filter change.

In `@front/plugins/db_cleanup/script.py`:
- Around line 163-169: Wrap the DevicesHistory cleanup in `cleanup_database()`
so a failure in `DevicesHistoryInstance().prune_history(DEV_HIST_DAYS)` does not
abort the rest of the database cleanup. Add a local try/except around the
existing `DEV_HIST_DAYS` block, log the exception with `mylog`, and then
continue to the remaining prune, dedupe, checkpoint, `ANALYZE`, and `VACUUM`
steps. Keep the behavior of the `DevicesHistoryInstance` path the same on
success, but make sure errors are isolated to this step only.
- Line 38: The DEV_HIST_DAYS setting in the db cleanup script is using a truthy
fallback that overrides an explicit 0 value. Update the assignment in the
module-level configuration near get_setting_value("DEV_HIST_DAYS") so it only
falls back to 14 when the setting is None, preserving 0 as a valid disable value
for prune_history().

In `@server/api_server/api_server_start.py`:
- Around line 776-789: The api_devices_history_filters REST handler currently
calls DevicesHistoryInstance().get_available_filter_values() without any
exception handling, so DB failures can bubble up as an unhandled Flask 500 and
break the JSON contract. Wrap the logic in api_devices_history_filters with
try/except similar to the GraphQL history resolvers, and on error return a JSON
response with success false and an empty or safe fallback data payload so the
frontend can keep using resp.data consistently.

In `@server/api_server/graphql_endpoint.py`:
- Around line 67-134: The history resolvers are passing the client-provided
limit through without an upper bound, unlike the pagination pattern used
elsewhere in this file. Update resolve_allDeviceHistoryGrouped (and match
resolve_deviceHistoryGrouped if needed) to clamp limit with the existing
_MAX_LIMIT before calling DevicesHistoryInstance methods, so large requests
cannot trigger oversized grouped-history payloads. Keep the change localized to
the resolver methods and preserve the current default limits.

In `@server/models/device_history_instance.py`:
- Around line 174-216: The grouped history pagination in _query_grouped
currently loads and groups every matching row before slicing, so move pagination
into the database instead of applying offset/limit only to grouped_list. Update
_query_grouped to first select the page of distinct group keys (timestamp,
changedBy, devGUID) with ORDER BY plus LIMIT/OFFSET, then fetch only the rows
for those keys to assemble each group’s changes. Keep the existing filter
behavior from _build_clauses and ensure any changedColumn filtering still
matches the intended group-selection semantics.

---

Nitpick comments:
In @.gemini/skills/skills-index/SKILL.md:
- Line 21: The `testing-workflow` row in `SKILL.md` should not mention the
devcontainer-only `testFailure` tool or other Copilot-specific tooling. Update
the `Testing` entry to keep the Gemini-side index environment-neutral while
still pointing to the richer counterpart, so the `testing-workflow` description
only references capabilities available in Gemini.

In @.gemini/skills/testing-workflow/SKILL.md:
- Around line 29-85: The command examples in the testing workflow skill
hard-code the repository root, which conflicts with the repo-relative
convention. Update the examples in SKILL.md to use relative paths for pytest and
setup commands, and ensure any references tied to the testing flow use the same
repo-root style consistently. Locate the affected examples around the testing
sections and the Authentication & Environment Reset steps.
- Around line 19-23: The testing-workflow skill currently contains a
Copilot-specific handoff that tells readers to call
activate_skill("devcontainer-management"), which is not valid Gemini CLI
guidance. Update the relevant guidance in SKILL.md to describe the devcontainer
step in plain language using Gemini-available tooling, or remove it from this
skill and relocate it to the Copilot-side skill; keep the surrounding
host-machine/container detection logic consistent with the existing
testing-workflow instructions.

In `@docs/SETTINGS_SYSTEM.md`:
- Around line 58-66: The SETTINGS_SYSTEM example only shows the `name`
localization hook and omits the matching `description` hook, so update the
example to demonstrate both references together. Use the existing localization
pattern in the doc around the `config.json` example to show how both
`<KEY>_name` and `<KEY>_description` should be declared, so readers can localize
plugin settings fully.

In `@server/api_server/graphql_endpoint.py`:
- Line 109: The broad exception handlers in the GraphQL endpoint are hiding real
failures and only logging a minimal message. Replace the two `except Exception`
blocks in the device history resolver path with narrower error handling or
re-raise unexpected errors, and keep returning `DeviceHistoryResult` only for
the intended “no data” cases. In the logging path, include full exception
context/traceback instead of just `{e}` so failures in the `graphql_endpoint`
flow are diagnosable.

In `@server/database.py`:
- Around line 242-247: The trigger recreation in database initialization is
currently compensating for AppEvent_obj.drop_all_triggers() wiping unrelated
triggers, which creates fragile coupling. Update the trigger cleanup flow so
AppEvent_obj.drop_all_triggers() only removes triggers it owns, or introduce an
explicit trigger registry/ownership check, and keep
ensure_deviceshistory_triggers(self.sql) as the device-history re-create path
tied to the database bootstrap sequence.

In `@server/db/db_history.py`:
- Around line 28-74: The tracked-field definitions are duplicated across
_HIST_FIELDS in db_history, _DEV_HIST_TRACKED in initialise, and the
DEV_HIST_TRACKED default, so they can drift apart. Introduce a single shared
source for the field-name list (for example, a constant module imported by
db_history and initialise) and have the settings default derive from that shared
list instead of hardcoding it in multiple places. Keep the existing symbols
_HIST_FIELDS and _DEV_HIST_TRACKED aligned through that shared constant.
- Around line 85-88: The Devices history trigger is repeatedly re-running the
same DEV_HIST_TRACKED lookup for each per-field block, causing many duplicate
Settings subqueries on every write. Update the db_history trigger generation
around _TRACKED_CHECK and the per-field INSERT..SELECT blocks to resolve tracked
fields once per trigger firing, ideally by using a dedicated lookup structure
such as DevHistTrackedFields or an equivalent precomputed EXISTS/join check, and
then reuse that result across all field writes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 131077e3-689c-435f-a899-245ba5aca21b

📥 Commits

Reviewing files that changed from the base of the PR and between 200bead and 35313fe.

📒 Files selected for processing (30)
  • .gemini/skills/initiative-start/reasearch-skill.md
  • .gemini/skills/plugin-development/plugin-skill.md
  • .gemini/skills/project-navigation/SKILL.md
  • .gemini/skills/settings/SKILL.md
  • .gemini/skills/skills-index/SKILL.md
  • .gemini/skills/testing-workflow/SKILL.md
  • .github/copilot-instructions.md
  • .github/skills/database-patterns/SKILL.md
  • .github/skills/mcp-activation/SKILL.md
  • .github/skills/settings-management/SKILL.md
  • .github/skills/skills-overview/SKILL.md
  • back/app.conf
  • docs/API_GRAPHQL.md
  • docs/SETTINGS_SYSTEM.md
  • front/change_history.php
  • front/deviceDetails.php
  • front/php/templates/header.php
  • front/php/templates/language/en_us.json
  • front/php/templates/skel_device_details_tab_history.php
  • front/plugins/db_cleanup/script.py
  • server/api_server/api_server_start.py
  • server/api_server/graphql_endpoint.py
  • server/api_server/graphql_types.py
  • server/database.py
  • server/db/db_history.py
  • server/initialise.py
  • server/models/device_history_instance.py
  • server/models/device_instance.py
  • test/backend/test_device_history.py
  • test/db_test_helpers.py

Comment thread .gemini/skills/initiative-start/reasearch-skill.md
Comment thread front/change_history.php Outdated
Comment thread front/change_history.php Outdated
Comment thread front/change_history.php Outdated
Comment thread front/change_history.php Outdated
Comment thread front/plugins/db_cleanup/script.py
Comment thread front/plugins/db_cleanup/script.py
Comment on lines +776 to +789
@app.route("/devices/history/filters", methods=["GET"])
@validate_request(
operation_id="get_device_history_filters",
summary="Get Device History Filter Values",
description="Return distinct changedBy and changedColumn values available in DevicesHistory. Optionally scope to a single device with ?devGuid=<guid>.",
tags=["devices"],
auth_callable=is_authorized
)
def api_devices_history_filters(payload=None):
dev_guid = request.args.get("devGuid") or None
filters = DevicesHistoryInstance().get_available_filter_values(devGUID=dev_guid)
return jsonify({"success": True, "data": filters})


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No error handling around the DB call.

Unlike the GraphQL history resolvers (which wrap the same kind of DB access in try/except and return a safe fallback), this REST handler has no error handling. An exception here (DB error, etc.) will surface as an unhandled Flask 500, likely non-JSON, breaking the frontend's resp.data assumption.

🛡️ Proposed fix
 def api_devices_history_filters(payload=None):
     dev_guid = request.args.get("devGuid") or None
-    filters = DevicesHistoryInstance().get_available_filter_values(devGUID=dev_guid)
-    return jsonify({"success": True, "data": filters})
+    try:
+        filters = DevicesHistoryInstance().get_available_filter_values(devGUID=dev_guid)
+        return jsonify({"success": True, "data": filters})
+    except Exception as e:
+        mylog("none", f"[api] api_devices_history_filters error: {e}")
+        return jsonify({"success": False, "data": {"changedBy": [], "changedColumn": []}}), 500
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@app.route("/devices/history/filters", methods=["GET"])
@validate_request(
operation_id="get_device_history_filters",
summary="Get Device History Filter Values",
description="Return distinct changedBy and changedColumn values available in DevicesHistory. Optionally scope to a single device with ?devGuid=<guid>.",
tags=["devices"],
auth_callable=is_authorized
)
def api_devices_history_filters(payload=None):
dev_guid = request.args.get("devGuid") or None
filters = DevicesHistoryInstance().get_available_filter_values(devGUID=dev_guid)
return jsonify({"success": True, "data": filters})
`@app.route`("/devices/history/filters", methods=["GET"])
`@validate_request`(
operation_id="get_device_history_filters",
summary="Get Device History Filter Values",
description="Return distinct changedBy and changedColumn values available in DevicesHistory. Optionally scope to a single device with ?devGuid=<guid>.",
tags=["devices"],
auth_callable=is_authorized
)
def api_devices_history_filters(payload=None):
dev_guid = request.args.get("devGuid") or None
try:
filters = DevicesHistoryInstance().get_available_filter_values(devGUID=dev_guid)
return jsonify({"success": True, "data": filters})
except Exception as e:
mylog("none", f"[api] api_devices_history_filters error: {e}")
return jsonify({"success": False, "data": {"changedBy": [], "changedColumn": []}}), 500
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api_server/api_server_start.py` around lines 776 - 789, The
api_devices_history_filters REST handler currently calls
DevicesHistoryInstance().get_available_filter_values() without any exception
handling, so DB failures can bubble up as an unhandled Flask 500 and break the
JSON contract. Wrap the logic in api_devices_history_filters with try/except
similar to the GraphQL history resolvers, and on error return a JSON response
with success false and an empty or safe fallback data payload so the frontend
can keep using resp.data consistently.

Comment thread server/api_server/graphql_endpoint.py
Comment thread server/models/device_history_instance.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/models/device_history_instance.py (2)

18-31: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Connections leak on query failure.

Neither _fetchall nor _execute close the connection if execute() raises (e.g., locked DB, malformed query). Under load/errors this can exhaust SQLite connections.

🔧 Proposed fix
     def _fetchall(self, query, params=()):
         conn = get_temp_db_connection()
-        rows = conn.execute(query, params).fetchall()
-        conn.close()
-        return [dict(r) for r in rows]
+        try:
+            rows = conn.execute(query, params).fetchall()
+            return [dict(r) for r in rows]
+        finally:
+            conn.close()

     def _execute(self, query, params=()):
         conn = get_temp_db_connection()
-        cur = conn.cursor()
-        cur.execute(query, params)
-        affected = cur.rowcount
-        conn.commit()
-        conn.close()
-        return affected
+        try:
+            cur = conn.cursor()
+            cur.execute(query, params)
+            affected = cur.rowcount
+            conn.commit()
+            return affected
+        finally:
+            conn.close()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/models/device_history_instance.py` around lines 18 - 31, Both
`_fetchall` and `_execute` can leak the temp SQLite connection when `execute()`
raises, so update these helpers to always close the connection even on failure.
Use `get_temp_db_connection()` in `DeviceHistoryInstance`, and wrap the
query/commit path in a try/finally so `conn.close()` runs regardless of success
or exception; keep the existing return values from `_fetchall` and `_execute`
unchanged.

118-136: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

get_total_group_count should take search into account.

get_grouped_history/get_all_grouped_history already filter by paging["search"], but get_total_group_count() still omits it, so count can exceed the number of groups actually shown. Thread search through the two get_total_group_count(...) calls in server/api_server/graphql_endpoint.py and add it to server/models/device_history_instance.py:get_total_group_count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/models/device_history_instance.py` around lines 118 - 136,
`get_total_group_count` is missing the `search` filter, so its count can differ
from the grouped history results. Update
`device_history_instance.get_total_group_count` to accept `search` and include
it in the clause-building logic alongside `devGUID`, `changedColumn`, and
`changedBy`, then thread the same `paging["search"]` argument through both calls
in `graphql_endpoint.py` that invoke `get_total_group_count`. Use the existing
`get_grouped_history` and `get_all_grouped_history` filtering pattern as the
reference so the count matches the displayed groups.
♻️ Duplicate comments (2)
server/api_server/graphql_endpoint.py (1)

78-158: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Missing upper bound on limit for history queries.

Same concern raised previously: deviceHistoryGrouped passes the client-supplied limit straight through to DevicesHistoryInstance without capping it, unlike apply_common_pagination elsewhere in this codebase which enforces min(options.limit, _MAX_LIMIT). Combined with _query_grouped fetching the full matching set before slicing, a large limit can force very large per-request payloads/memory use.

⚡ Proposed fix
         try:
             mylog("none", f"[HISTORY] unified resolver devGUID={devGUID} changedColumn={changedColumn} changedBy={changedBy}")

             h = DevicesHistoryInstance()
             paging = extract_paging(options)
+            paging["limit"] = min(paging["limit"], _MAX_LIMIT)

Best applied inside extract_paging itself (see companion note in graphql_helpers.py) so all history callers get the cap consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api_server/graphql_endpoint.py` around lines 78 - 158, The unified
history resolver is forwarding the client’s `limit` from `extract_paging` into
`resolve_deviceHistoryGrouped` without any maximum cap, so large requests can
still overwhelm `DevicesHistoryInstance`. Update `extract_paging` to enforce the
same upper bound used by `apply_common_pagination` (cap `options.limit` with
`_MAX_LIMIT`) so `resolve_deviceHistoryGrouped`, `get_grouped_history`, and
`get_all_grouped_history` all receive bounded pagination values consistently.
server/models/device_history_instance.py (1)

196-223: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Grouped-history pagination still fetches the entire filtered result set before slicing.

This is the same concern raised on a prior commit: _query_grouped issues no SQL LIMIT/OFFSET, fetching every row matching the filter before grouping, sorting, and only then slicing grouped_list[start:end]. Cost is O(total matching rows) per page request, and the table grows monotonically since triggers insert a row per changed tracked field on every device update.

💡 Possible direction (unchanged from previous review)

Push pagination into SQL by first selecting the page of distinct group keys (timestamp, changedBy, devGUID) with ORDER BY ... LIMIT ? OFFSET ?, then fetching only the detail rows for those keys — while preserving current filter semantics (e.g. whether changedColumn narrows the "changes" list vs. only gates group inclusion).

Also applies to: 279-285

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/models/device_history_instance.py` around lines 196 - 223,
_query_grouped in device_history_instance.py still paginates after fetching all
matching history rows, so move pagination into SQL instead of slicing
grouped_list in Python. Update _query_grouped to first select the page of
distinct group keys (timestamp, changedBy, devGUID) using the existing filters
from _build_clauses plus ORDER BY and LIMIT/OFFSET, then fetch only the rows for
those keys and build the grouped result. Preserve the current
changedColumn/search semantics and keep the public query methods that rely on
_query_grouped aligned with the new SQL-based pagination.
🧹 Nitpick comments (5)
server/api_server/graphql_helpers.py (1)

144-167: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider enforcing _MAX_LIMIT here rather than in each caller.

extract_paging is the shared entry point for history paging, and this file already defines _MAX_LIMIT/_DEFAULT_LIMIT used by apply_common_pagination. Applying the cap here once would close the missing-limit-bound gap in resolve_deviceHistoryGrouped (see companion comment in graphql_endpoint.py) for all current and future callers.

♻️ Proposed refactor
 def extract_paging(options):
     if not options:
         return {
             "page": 1,
-            "limit": 50,
+            "limit": _DEFAULT_LIMIT,
             "offset": 0,
             "sort": [],
             "search": None
         }

     page = getattr(options, "page", 1) or 1
-    limit = getattr(options, "limit", 50) or 50
+    limit = min(getattr(options, "limit", _DEFAULT_LIMIT) or _DEFAULT_LIMIT, _MAX_LIMIT)
     search = getattr(options, "search", None)
     sort = getattr(options, "sort", []) or []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api_server/graphql_helpers.py` around lines 144 - 167,
`extract_paging` is the shared pagination entry point, but it currently returns
the raw requested limit without enforcing `_MAX_LIMIT`. Update `extract_paging`
to clamp the computed `limit` against `_MAX_LIMIT` (while preserving the
existing default behavior), so all callers including
`resolve_deviceHistoryGrouped` automatically get the cap; use the existing
`_MAX_LIMIT` and `_DEFAULT_LIMIT` symbols in `graphql_helpers.py` to keep the
behavior centralized.
server/models/device_history_instance.py (1)

209-213: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove or downgrade leftover debug logging.

These mylog("none", ...) calls (including a dev-time type(sort) dump) run unconditionally on every history query. "none" isn't used elsewhere in this file for diagnostics — prune_history uses "verbose". Recommend removing these or downgrading to "trace"/"verbose" before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/models/device_history_instance.py` around lines 209 - 213, The history
query path in device_history_instance is still emitting unconditional debug logs
via mylog("none", ...), including the type(sort) dump, which should not ship
as-is. Remove these leftover diagnostics or downgrade them to the existing
verbose/trace style used elsewhere in the file, and update the logging calls
around the history query flow so they no longer run at the current level on
every request.
front/changeLogCore.php (3)

126-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove leftover debug console.log.

🧹 Proposed fix
-            console.log(query)
-
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @front/changeLogCore.php at line 126, Remove the leftover debug logging in
the query handling path by deleting the console.log call inside the relevant
change-log logic in changeLogCore.php. Locate the query-related code path around
the console.log(query) statement and ensure no temporary debug output remains in
the final implementation.


</details>

<!-- cr-comment:v1:ce9fadc8412e8fd6b08f6ca3 -->

---

`247-286`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _💤 Low value_

**Dead code: unused GraphQL `query` in `populateFilters`.**

The `query` variable (Lines 248-254) references a resolver named `allDeviceHistoryGrouped` that doesn't appear to exist (the actual resolver elsewhere in this file is `deviceHistoryGrouped`). It's built but never sent — `populateFilters` uses the REST endpoint at Line 265 instead. Remove the dead code.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @front/changeLogCore.php around lines 247 - 286, The GraphQL query built at
the start of populateFilters is dead code because it is never used and
references the wrong resolver name. Remove the unused query block from
populateFilters, keeping the existing AJAX call to the filters endpoint and its
URL-building logic intact, and leave the rest of the filter population flow
unchanged.


</details>

<!-- cr-comment:v1:b72e143e6d5e51086c1e18d5 -->

---

`296-300`: _🚀 Performance & Scalability_ | _🔵 Trivial_ | _⚡ Quick win_

**Potential duplicate initial data fetch on page load.**

`initializeHistoryTable()` is called synchronously right after firing `populateFilters()`. Since DataTables with `serverSide: true` triggers its own `ajax` call on initialization, and `populateFilters()`'s async success handler (Line 283) also explicitly calls `.ajax.reload()` once filters arrive, the table's data is very likely fetched twice on every page load.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @front/changeLogCore.php around lines 296 - 300, Potential duplicate data
fetch during page load: initChangeLog() calls initializeHistoryTable()
immediately after populateFilters(), but populateFilters() later triggers its
own table reload once filters are ready. Update initChangeLog() and/or
populateFilters() so the History table is initialized or reloaded only once per
load, using initializeHistoryTable() and the populateFilters() success callback
to coordinate the first ajax call.


</details>

<!-- cr-comment:v1:024329a79cadcd760d1fe2d4 -->

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @front/changeLogCore.php:

  • Around line 128-171: Update the AJAX handler in changeLogCore.php so both
    failure paths clear the UI state and GraphQL errors are handled. In the $.ajax
    success callback, check resp.errors before reading
    resp.data.deviceHistoryGrouped; if present, treat it like a failure and return
    empty results. In the error callback, after invoking callback with empty data,
    also call hideSpinner() and hideChangeLogSkeleton() so the table overlay is
    removed on request failure. Keep the existing success flow in the same AJAX
    block that uses getSetting("API_TOKEN"), callback, hideSpinner, and
    hideChangeLogSkeleton.
  • Around line 66-72: devGUID is being assigned without a declaration, which
    creates an implicit global and can collide with other scripts loaded by
    changeLog.php and deviceDetails.php. Declare devGUID explicitly in the
    scope where it is used in the change-log request handling, and do the same for
    url inside populateFilters() so both identifiers are local rather than
    global.
  • Around line 195-200: The render callback for the device row is inserting
    unescaped values returned by getDevDataByGuid into both the anchor href and link
    text, creating an XSS risk. Update the render function to escape the devMac and
    devName results before interpolating them into the HTML, not just the GUID
    passed into getDevDataByGuid. Keep the existing escHtml usage for the data GUID
    and apply the same escaping consistently to the returned values in this column.

In @front/deviceDetails.php:

  • Around line 170-175: The History tab content is being initialized too early
    because changeLogCore.php runs initChangeLog() on $(document).ready, causing
    unnecessary GraphQL and /devices/history/filters requests on page load. Update
    the device details flow so the History tab’s setup is deferred until the tab is
    first opened by hooking initChangeLog() behind the tab’s first shown.bs.tab
    event, and keep the existing changeLogCore.php logic reachable from that
    trigger.

Outside diff comments:
In @server/models/device_history_instance.py:

  • Around line 18-31: Both _fetchall and _execute can leak the temp SQLite
    connection when execute() raises, so update these helpers to always close the
    connection even on failure. Use get_temp_db_connection() in
    DeviceHistoryInstance, and wrap the query/commit path in a try/finally so
    conn.close() runs regardless of success or exception; keep the existing return
    values from _fetchall and _execute unchanged.
  • Around line 118-136: get_total_group_count is missing the search filter,
    so its count can differ from the grouped history results. Update
    device_history_instance.get_total_group_count to accept search and include
    it in the clause-building logic alongside devGUID, changedColumn, and
    changedBy, then thread the same paging["search"] argument through both calls
    in graphql_endpoint.py that invoke get_total_group_count. Use the existing
    get_grouped_history and get_all_grouped_history filtering pattern as the
    reference so the count matches the displayed groups.

Duplicate comments:
In @server/api_server/graphql_endpoint.py:

  • Around line 78-158: The unified history resolver is forwarding the client’s
    limit from extract_paging into resolve_deviceHistoryGrouped without any
    maximum cap, so large requests can still overwhelm DevicesHistoryInstance.
    Update extract_paging to enforce the same upper bound used by
    apply_common_pagination (cap options.limit with _MAX_LIMIT) so
    resolve_deviceHistoryGrouped, get_grouped_history, and
    get_all_grouped_history all receive bounded pagination values consistently.

In @server/models/device_history_instance.py:

  • Around line 196-223: _query_grouped in device_history_instance.py still
    paginates after fetching all matching history rows, so move pagination into SQL
    instead of slicing grouped_list in Python. Update _query_grouped to first select
    the page of distinct group keys (timestamp, changedBy, devGUID) using the
    existing filters from _build_clauses plus ORDER BY and LIMIT/OFFSET, then fetch
    only the rows for those keys and build the grouped result. Preserve the current
    changedColumn/search semantics and keep the public query methods that rely on
    _query_grouped aligned with the new SQL-based pagination.

Nitpick comments:
In @front/changeLogCore.php:

  • Line 126: Remove the leftover debug logging in the query handling path by
    deleting the console.log call inside the relevant change-log logic in
    changeLogCore.php. Locate the query-related code path around the
    console.log(query) statement and ensure no temporary debug output remains in the
    final implementation.
  • Around line 247-286: The GraphQL query built at the start of populateFilters
    is dead code because it is never used and references the wrong resolver name.
    Remove the unused query block from populateFilters, keeping the existing AJAX
    call to the filters endpoint and its URL-building logic intact, and leave the
    rest of the filter population flow unchanged.
  • Around line 296-300: Potential duplicate data fetch during page load:
    initChangeLog() calls initializeHistoryTable() immediately after
    populateFilters(), but populateFilters() later triggers its own table reload
    once filters are ready. Update initChangeLog() and/or populateFilters() so the
    History table is initialized or reloaded only once per load, using
    initializeHistoryTable() and the populateFilters() success callback to
    coordinate the first ajax call.

In @server/api_server/graphql_helpers.py:

  • Around line 144-167: extract_paging is the shared pagination entry point,
    but it currently returns the raw requested limit without enforcing _MAX_LIMIT.
    Update extract_paging to clamp the computed limit against _MAX_LIMIT
    (while preserving the existing default behavior), so all callers including
    resolve_deviceHistoryGrouped automatically get the cap; use the existing
    _MAX_LIMIT and _DEFAULT_LIMIT symbols in graphql_helpers.py to keep the
    behavior centralized.

In @server/models/device_history_instance.py:

  • Around line 209-213: The history query path in device_history_instance is
    still emitting unconditional debug logs via mylog("none", ...), including the
    type(sort) dump, which should not ship as-is. Remove these leftover diagnostics
    or downgrade them to the existing verbose/trace style used elsewhere in the
    file, and update the logging calls around the history query flow so they no
    longer run at the current level on every request.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Path: .coderabbit.yaml

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `bcbc008e-614d-4199-8c2f-8e4076e33dae`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 35313feff4bff19cd40fb00d189818ea2fb84a84 and e0449bed8e69bdd2d6ac43b00a8a31d60d97d51c.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `docs/img/DEVICE_MANAGEMENT/device_presence.png` is excluded by `!**/*.png`

</details>

<details>
<summary>📒 Files selected for processing (42)</summary>

* `back/app.conf`
* `docs/API_GRAPHQL.md`
* `docs/PERFORMANCE.md`
* `docs/WHERE_NETALERTX_FITS.md`
* `docs/index.md`
* `front/changeLog.php`
* `front/changeLogCore.php`
* `front/css/app.css`
* `front/deviceDetails.php`
* `front/js/cache.js`
* `front/php/templates/header.php`
* `front/php/templates/language/ar_ar.json`
* `front/php/templates/language/ca_ca.json`
* `front/php/templates/language/cs_cz.json`
* `front/php/templates/language/de_de.json`
* `front/php/templates/language/en_us.json`
* `front/php/templates/language/es_es.json`
* `front/php/templates/language/fa_fa.json`
* `front/php/templates/language/fi_fi.json`
* `front/php/templates/language/fr_fr.json`
* `front/php/templates/language/he_il.json`
* `front/php/templates/language/id_id.json`
* `front/php/templates/language/it_it.json`
* `front/php/templates/language/ja_jp.json`
* `front/php/templates/language/nb_no.json`
* `front/php/templates/language/pl_pl.json`
* `front/php/templates/language/pt_br.json`
* `front/php/templates/language/pt_pt.json`
* `front/php/templates/language/ru_ru.json`
* `front/php/templates/language/sv_sv.json`
* `front/php/templates/language/tr_tr.json`
* `front/php/templates/language/uk_ua.json`
* `front/php/templates/language/vi_vn.json`
* `front/php/templates/language/zh_cn.json`
* `front/php/templates/skel_change_log.php`
* `front/pluginsCore.php`
* `server/api_server/api_server_start.py`
* `server/api_server/graphql_endpoint.py`
* `server/api_server/graphql_helpers.py`
* `server/api_server/graphql_types.py`
* `server/initialise.py`
* `server/models/device_history_instance.py`

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (13)</summary>

* front/php/templates/skel_change_log.php
* front/pluginsCore.php
* docs/index.md
* front/php/templates/language/pl_pl.json
* docs/PERFORMANCE.md
* front/php/templates/language/fa_fa.json
* front/php/templates/language/es_es.json
* front/php/templates/language/nb_no.json
* front/php/templates/language/pt_br.json
* front/php/templates/language/ar_ar.json
* front/php/templates/language/fi_fi.json
* front/php/templates/language/ru_ru.json
* docs/API_GRAPHQL.md

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* back/app.conf
* front/php/templates/header.php
* server/api_server/api_server_start.py
* server/api_server/graphql_types.py
* server/initialise.py

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread front/changeLogCore.php Outdated
Comment thread front/changeLogCore.php Outdated
Comment thread front/changeLogCore.php Outdated
Comment thread front/deviceDetails.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
front/changeLogCore.php (2)

256-269: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Stored XSS: devMac/devName still interpolated unescaped.

Same issue flagged previously: escHtml() is applied only to guid (used as the lookup key), not to the mac/name values returned by getDevDataByGuid. Both are interpolated directly into the anchor's href and text without escaping — a stored XSS vector since device names are user-editable.

🔒️ Proposed fix
           targets: 2,
           render: function (data) {

             const guid = escHtml(data);
-            const mac = getDevDataByGuid(guid, "devMac");
-            const name = getDevDataByGuid(guid, "devName");
+            const mac = getDevDataByGuid(data, "devMac") || "";
+            const name = getDevDataByGuid(data, "devName") || "";

             return `
-              <a href="deviceDetails.php?mac=${mac}" data-guid="${guid}">
-                ${name} (${guid})
+              <a href="deviceDetails.php?mac=${encodeURIComponent(mac)}" data-guid="${guid}">
+                ${escHtml(name)} (${guid})
               </a>`;
           }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/changeLogCore.php` around lines 256 - 269, The device link renderer in
changeLogCore.php still interpolates unescaped values from getDevDataByGuid for
devMac and devName, creating a stored XSS risk. Update the render function to
escape both the href parameter and the displayed name before building the
anchor, while keeping escHtml on the guid lookup key. Use the existing
getDevDataByGuid and escHtml flow in the table renderer to locate and fix the
unsafe interpolation.

193-233: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

AJAX error path still leaves spinner/skeleton stuck; GraphQL errors still unchecked.

This still has the two gaps flagged in a previous review:

  1. error: (lines 207-233) never calls hideSpinner()/hideChangeLogSkeleton(), so a failed request leaves the overlay/spinner visible indefinitely.
  2. success: (lines 193-205) never inspects resp.errors — GraphQL can return HTTP 200 with a top-level errors array and null/partial data, which would silently render an empty table.
🔧 Proposed fix
           success: function (resp) {
+            if (resp?.errors?.length) {
+              console.error("History GraphQL error:", resp.errors);
+            }
             const result = resp?.data?.deviceHistoryGrouped;

             callback({
               data: result?.history || [],
               recordsTotal: result?.count || 0,
               recordsFiltered: result?.count || 0
             });

             hideSpinner();
             hideChangeLogSkeleton();
           },
...
             callback({
               data: [],
               recordsTotal: 0,
               recordsFiltered: 0
             });
+            hideSpinner();
+            hideChangeLogSkeleton();
           }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/changeLogCore.php` around lines 193 - 233, In the AJAX flow inside the
changelog request handler, make sure both the success and error paths fully
clear the loading UI. Update the success callback to inspect resp.errors before
using resp.data.deviceHistoryGrouped, and if GraphQL returned errors, log them
and treat the response as failed/empty. Also add hideSpinner() and
hideChangeLogSkeleton() to the error callback so the spinner/skeleton are always
dismissed even when the request fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front/changeLogCore.php`:
- Around line 313-324: The URL built in populateFilters is malformed because
apiEndpoint already includes a leading slash and the current
`${apiBaseUrl}/${apiEndpoint}` concatenation creates a double slash, while
`encodeURIComponent(queryString)` wrongly encodes the whole query string and
hides `devGUID` from the server. Update the URL construction in populateFilters
so the base path and endpoint are joined without duplicating slashes, and only
encode the query parameter value returned by getDevDataByMac(urlMac, "devGUID")
rather than the entire query string. Use populateFilters, getApiBase, and
getDevDataByMac as the key symbols when adjusting this logic.

---

Duplicate comments:
In `@front/changeLogCore.php`:
- Around line 256-269: The device link renderer in changeLogCore.php still
interpolates unescaped values from getDevDataByGuid for devMac and devName,
creating a stored XSS risk. Update the render function to escape both the href
parameter and the displayed name before building the anchor, while keeping
escHtml on the guid lookup key. Use the existing getDevDataByGuid and escHtml
flow in the table renderer to locate and fix the unsafe interpolation.
- Around line 193-233: In the AJAX flow inside the changelog request handler,
make sure both the success and error paths fully clear the loading UI. Update
the success callback to inspect resp.errors before using
resp.data.deviceHistoryGrouped, and if GraphQL returned errors, log them and
treat the response as failed/empty. Also add hideSpinner() and
hideChangeLogSkeleton() to the error callback so the spinner/skeleton are always
dismissed even when the request fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5d87ad9b-cd28-427e-a9f0-d7661b026cd5

📥 Commits

Reviewing files that changed from the base of the PR and between e0449be and 887de65.

⛔ Files ignored due to path filters (1)
  • docs/img/DEVICE_MANAGEMENT/device_change_log.png is excluded by !**/*.png
📒 Files selected for processing (14)
  • docs/DEVICE_CHANGE_LOG.md
  • docs/DEVICE_PRESENCE.md
  • docs/WHERE_NETALERTX_FITS.md
  • docs/stylesheets/custom.css
  • front/changeLog.php
  • front/changeLogCore.php
  • front/css/app.css
  • front/deviceDetailsEvents.php
  • front/deviceDetailsPresence.php
  • front/js/common.js
  • front/js/ui_components.js
  • front/php/templates/language/en_us.json
  • front/presence.php
  • mkdocs.yml
💤 Files with no reviewable changes (1)
  • front/changeLog.php
✅ Files skipped from review due to trivial changes (6)
  • docs/stylesheets/custom.css
  • front/js/ui_components.js
  • front/deviceDetailsPresence.php
  • docs/DEVICE_PRESENCE.md
  • front/php/templates/language/en_us.json
  • docs/WHERE_NETALERTX_FITS.md

Comment thread front/changeLogCore.php Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front/css/app.css`:
- Around line 2711-2715: The selector list in app.css incorrectly uses a
descendant selector for panHistory, so position: relative only applies under
appEvents. Update the grouped rule that includes `#settingsPage`, `#devicesPage`,
`#panDetails`, and `#panEvents` to make `#panHistory` a standalone selector with a
comma instead of `#appEvents` `#panHistory`, so `#change-log-skeleton` anchors
correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8a9633cc-0f7e-4765-adef-1778ddc668a5

📥 Commits

Reviewing files that changed from the base of the PR and between 887de65 and 2dd7904.

📒 Files selected for processing (19)
  • front/changeLogCore.php
  • front/css/app.css
  • front/php/templates/language/ar_ar.json
  • front/php/templates/language/de_de.json
  • front/php/templates/language/es_es.json
  • front/php/templates/language/fr_fr.json
  • front/php/templates/language/it_it.json
  • front/php/templates/language/ja_jp.json
  • front/php/templates/language/nb_no.json
  • front/php/templates/language/pl_pl.json
  • front/php/templates/language/ru_ru.json
  • front/php/templates/language/tr_tr.json
  • front/php/templates/language/uk_ua.json
  • front/php/templates/language/zh_cn.json
  • front/php/templates/skel_change_log.php
  • front/pluginsCore.php
  • front/workflows.php
  • server/api_server/graphql_endpoint.py
  • server/models/device_history_instance.py
✅ Files skipped from review due to trivial changes (8)
  • front/php/templates/skel_change_log.php
  • front/php/templates/language/tr_tr.json
  • front/php/templates/language/nb_no.json
  • front/php/templates/language/de_de.json
  • front/php/templates/language/ar_ar.json
  • front/php/templates/language/pl_pl.json
  • front/php/templates/language/fr_fr.json
  • front/php/templates/language/uk_ua.json
🚧 Files skipped from review as they are similar to previous changes (8)
  • front/php/templates/language/ru_ru.json
  • server/api_server/graphql_endpoint.py
  • front/php/templates/language/it_it.json
  • front/php/templates/language/es_es.json
  • server/models/device_history_instance.py
  • front/php/templates/language/zh_cn.json
  • front/changeLogCore.php
  • front/php/templates/language/ja_jp.json

Comment thread front/css/app.css
Comment on lines 2711 to 2715
#settingsPage, #devicesPage, #deviceDetailsPage,
#eventsPage, #presencePage, #reportPage, #notifications, #wf-content-wrapper, #pluginsPage,
#panDetails, #panSessions, #panPresence, #panEvents, #appEvents {
#panDetails, #panSessions, #panPresence, #panEvents, #appEvents #panHistory{
position: relative;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add a comma before #panHistory. #appEvents #panHistory`` is a descendant selector, so position: relative only applies when `#panHistory` sits inside `#appEvents`. If `#panHistory` is a standalone pane, `#change-log-skeleton` can anchor to the wrong ancestor.

🐛 Proposed fix
-#panDetails, `#panSessions`, `#panPresence`, `#panEvents`, `#appEvents` `#panHistory`{
+#panDetails, `#panSessions`, `#panPresence`, `#panEvents`, `#appEvents`, `#panHistory`{
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#settingsPage, #devicesPage, #deviceDetailsPage,
#eventsPage, #presencePage, #reportPage, #notifications, #wf-content-wrapper, #pluginsPage,
#panDetails, #panSessions, #panPresence, #panEvents, #appEvents {
#panDetails, #panSessions, #panPresence, #panEvents, #appEvents #panHistory{
position: relative;
}
`#settingsPage`, `#devicesPage`, `#deviceDetailsPage`,
`#eventsPage`, `#presencePage`, `#reportPage`, `#notifications`, `#wf-content-wrapper`, `#pluginsPage`,
`#panDetails`, `#panSessions`, `#panPresence`, `#panEvents`, `#appEvents`, `#panHistory`{
position: relative;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front/css/app.css` around lines 2711 - 2715, The selector list in app.css
incorrectly uses a descendant selector for panHistory, so position: relative
only applies under appEvents. Update the grouped rule that includes
`#settingsPage`, `#devicesPage`, `#panDetails`, and `#panEvents` to make `#panHistory` a
standalone selector with a comma instead of `#appEvents` `#panHistory`, so
`#change-log-skeleton` anchors correctly.

@jokob-sk jokob-sk merged commit 86440b4 into main Jul 6, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant